/**
 * Copyright (c) 2013-2020 Contributors to the Eclipse Foundation
 *
 * <p> See the NOTICE file distributed with this work for additional information regarding copyright
 * ownership. All rights reserved. This program and the accompanying materials are made available
 * under the terms of the Apache License, Version 2.0 which accompanies this distribution and is
 * available at http://www.apache.org/licenses/LICENSE-2.0.txt
 */
package org.locationtech.geowave.core.store.query.constraints;

import java.util.HashMap;
import java.util.Map;
import org.locationtech.geowave.core.index.ByteArray;
import org.locationtech.geowave.core.index.Coordinate;
import org.locationtech.geowave.core.index.CoordinateRange;
import org.locationtech.geowave.core.index.MultiDimensionalCoordinateRanges;
import org.locationtech.geowave.core.index.MultiDimensionalCoordinateRangesArray;
import org.locationtech.geowave.core.index.MultiDimensionalCoordinates;

public class CoordinateRangeUtils {
  public static interface RangeCache {
    public boolean inBounds(final MultiDimensionalCoordinates coordinates);
  }

  private static interface RangeByBinIdCache {
    public boolean inBounds(final Coordinate coordinate);
  }

  public static class RangeLookupFactory {
    public static RangeCache createMultiRangeLookup(
        final MultiDimensionalCoordinateRangesArray[] coordinateRanges) {
      if ((coordinateRanges == null) || (coordinateRanges.length == 0)) {
        return new NullRangeLookup();
      } else if (coordinateRanges.length == 1) {
        return createRangeLookup(coordinateRanges[0].getRangesArray());
      } else {
        return new MultiRangeCacheLookup(coordinateRanges);
      }
    }

    public static RangeCache createRangeLookup(
        final MultiDimensionalCoordinateRanges[] coordinateRanges) {
      if (coordinateRanges == null) {
        return new NullRangeLookup();
      } else if ((coordinateRanges.length == 1)
          && (coordinateRanges[0].getMultiDimensionalId() == null)) {
        return new SingleRangeLookup(coordinateRanges[0]);
      } else {
        return new MultiRangeLookup(coordinateRanges);
      }
    }
  }

  private static class MultiRangeCacheLookup implements RangeCache {
    private final RangeCache[] rangeCaches;

    public MultiRangeCacheLookup(final MultiDimensionalCoordinateRangesArray[] coordinateRanges) {
      rangeCaches = new RangeCache[coordinateRanges.length];
      for (int i = 0; i < coordinateRanges.length; i++) {
        rangeCaches[i] = RangeLookupFactory.createRangeLookup(coordinateRanges[i].getRangesArray());
      }
    }

    @Override
    public boolean inBounds(final MultiDimensionalCoordinates coordinates) {
      // this should act as an OR clause
      for (final RangeCache r : rangeCaches) {
        if (r.inBounds(coordinates)) {
          return true;
        }
      }
      return false;
    }
  }

  private static class NullRangeLookup implements RangeCache {
    @Override
    public boolean inBounds(final MultiDimensionalCoordinates coordinates) {
      return false;
    }
  }

  private static class SingleRangeLookup implements RangeCache {
    private final MultiDimensionalBinLookup singleton;

    public SingleRangeLookup(final MultiDimensionalCoordinateRanges coordinateRanges) {
      singleton = new MultiDimensionalBinLookup(coordinateRanges);
    }

    @Override
    public boolean inBounds(final MultiDimensionalCoordinates coordinates) {
      return inBounds(coordinates, singleton);
    }

    private static boolean inBounds(
        final MultiDimensionalCoordinates coordinates,
        final MultiDimensionalBinLookup binLookup) {
      final CoordinateRange[] retVal = new CoordinateRange[coordinates.getNumDimensions()];
      for (int d = 0; d < retVal.length; d++) {
        final Coordinate c = coordinates.getCoordinate(d);
        if (!binLookup.inBounds(d, c)) {
          return false;
        }
      }
      return true;
    }
  }

  private static class MultiRangeLookup implements RangeCache {
    private final Map<ByteArray, MultiDimensionalBinLookup> multiDimensionalIdToRangeMap;

    public MultiRangeLookup(final MultiDimensionalCoordinateRanges[] coordinateRanges) {
      multiDimensionalIdToRangeMap = new HashMap<>();
      for (final MultiDimensionalCoordinateRanges r : coordinateRanges) {
        multiDimensionalIdToRangeMap.put(
            new ByteArray(r.getMultiDimensionalId()),
            new MultiDimensionalBinLookup(r));
      }
    }

    @Override
    public boolean inBounds(final MultiDimensionalCoordinates coordinates) {
      final MultiDimensionalBinLookup binLookup =
          multiDimensionalIdToRangeMap.get(new ByteArray(coordinates.getMultiDimensionalId()));
      if (binLookup == null) {
        return false;
      }

      return SingleRangeLookup.inBounds(coordinates, binLookup);
    }
  }

  private static class BinLookupFactory {
    public static RangeByBinIdCache createBinLookup(final CoordinateRange[] coordinateRanges) {
      if (coordinateRanges == null) {
        return new NullBinLookup();
      } else if ((coordinateRanges.length == 1) && (coordinateRanges[0].getBinId() == null)) {
        return new SingleBinLookup(coordinateRanges[0]);
      } else {
        return new MultiBinLookup(coordinateRanges);
      }
    }
  }

  private static class MultiDimensionalBinLookup {
    private final RangeByBinIdCache[] rangePerDimensionCache;

    private MultiDimensionalBinLookup(final MultiDimensionalCoordinateRanges ranges) {
      rangePerDimensionCache = new RangeByBinIdCache[ranges.getNumDimensions()];
      for (int d = 0; d < rangePerDimensionCache.length; d++) {
        rangePerDimensionCache[d] =
            BinLookupFactory.createBinLookup(ranges.getRangeForDimension(d));
      }
    }

    public boolean inBounds(final int dimension, final Coordinate coordinate) {
      return rangePerDimensionCache[dimension].inBounds(coordinate);
    }
  }

  private static class NullBinLookup implements RangeByBinIdCache {

    @Override
    public boolean inBounds(final Coordinate coordinate) {
      return false;
    }
  }

  private static class SingleBinLookup implements RangeByBinIdCache {
    private final CoordinateRange singleton;

    public SingleBinLookup(final CoordinateRange singleton) {
      this.singleton = singleton;
    }

    @Override
    public boolean inBounds(final Coordinate coordinate) {
      return inBounds(singleton, coordinate);
    }

    private static boolean inBounds(final CoordinateRange range, final Coordinate coordinate) {
      final long coord = coordinate.getCoordinate();
      return (range.getMinCoordinate() <= coord) && (range.getMaxCoordinate() >= coord);
    }
  }

  private static class MultiBinLookup implements RangeByBinIdCache {
    private final Map<ByteArray, CoordinateRange> binIdToRangeMap;

    public MultiBinLookup(final CoordinateRange[] coordinateRanges) {
      binIdToRangeMap = new HashMap<>();
      for (final CoordinateRange r : coordinateRanges) {
        binIdToRangeMap.put(new ByteArray(r.getBinId()), r);
      }
    }

    @Override
    public boolean inBounds(final Coordinate coordinate) {
      final CoordinateRange range = binIdToRangeMap.get(new ByteArray(coordinate.getBinId()));
      if (range == null) {
        return false;
      }

      return SingleBinLookup.inBounds(range, coordinate);
    }
  }
}
